﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Diagnostics;

namespace Citrix.XaAdminMobile.Util
{
    /// <summary>
    /// Used to perform function tracing. The typical usage for this class is to acquire an
    /// instance using Enter, perform your tracing and then call Exit on the instance at the
    /// end of the function. Once you have acquired an instance via Enter it is assumed you
    /// will only ever call on that instance from the same thread (nothing should break if you
    /// do, however this certainly is not the expected usage).
    /// 
    /// If you just want to trace a single statement without the entry/exit tracing you can use
    /// the static version of the Log function.
    /// 
    /// Since we are dealing with managed code, if you want to use this trace class it is
    /// recommended you have a single point of return in your function so you only have to call
    /// Exit once (i.e. no deterministic finalisation).
    /// </summary>
    /// <remarks>Where ever a MethodBase instance is required you should call
    /// System.Reflection.MethodBase.GetCurrent() to get the current method being traced.</remarks>
    public class FTrace
    {
        /// <summary>
        /// We expect to do a lot of tracing, so we cache instances of this class per thread
        /// which we will reuse. The caching will only occur if paired Enter/Exit calls are
        /// made. If Exit never gets called then the relevant instance will be GC'd and any
        /// call to Enter will create a new instance (not recommended but perfectly acceptable).
        /// </summary>
        [ThreadStatic]
        private static Queue<FTrace> m_cachedContext;

        /// <summary>
        /// Used internally to build various strings. We use a per-thread static to cache
        /// builders to avoid repeated create/destroys to better support high volumes of
        /// tracing.
        /// </summary>
        [ThreadStatic]
        private static StringBuilder m_builder;

        /// <summary>
        /// This is the cached prefix to all our trace statements which contains the fully
        /// qualified function name.
        /// </summary>
        private string m_function;

        /// <summary>
        /// The optional trace level. By default no level is used. However if specified then the
        /// categories of all the trace statements will contain this level number.
        /// </summary>
        private uint? m_level;

        /// <summary>
        /// The cached category to use for (non entry/exit) trace statements.
        /// </summary>
        private string m_category = DefaultCategory;

        /// <summary>
        /// The default category to use for (non entry/exit) trace statements.
        /// </summary>
        private const string DefaultCategory = "Trace";

        /// <summary>
        /// When we have an error when attempting to build a trace statement we will instead
        /// trace a message using this category.
        /// </summary>
        private const string TraceErrorCategory = "TraceError";

        /// <summary>
        /// The category used for debug traces.
        /// </summary>
        private const string DebugCategory = "Debug";

        /// <summary>
        /// Define a private ctor so users have to call Enter.
        /// </summary>
        private FTrace()
        {
            // empty
        }

        /// <summary>
        /// Accessor to the (thread) static string builder. We use this so we can JIT create
        /// the thread static instance.
        /// </summary>
        private static StringBuilder Builder
        {
            get
            {
                if (m_builder == null)
                {
                    m_builder = new StringBuilder();
                }
                return m_builder;
            }
        }

        /// <summary>
        /// Pulls a trace context out of the cache if available, otherwise creates a new one.
        /// </summary>
        /// <returns>A trace context instance initialised with sensible defaults.</returns>
        private static FTrace GetTraceContext(MethodBase method)
        {
            FTrace trace = null;

            if (m_cachedContext == null)
            {
                m_cachedContext = new Queue<FTrace>();
            }

            if (m_cachedContext.Count > 0)
            {
                trace = m_cachedContext.Dequeue();
            }
            else
            {
                trace = new FTrace();
            }
            trace.m_level = null;
            trace.m_category = DefaultCategory;
            trace.m_function = GetMethodPreface(method);
            return trace;
        }

        /// <summary>
        /// Constructs the fully qualified method preface string given the specified method.
        /// </summary>
        /// <param name="method">The method to construct the preface for.</param>
        /// <returns>The method preface.</returns>
        private static string GetMethodPreface(MethodBase method)
        {
            StringBuilder builder = Builder;

            builder.Remove(0, builder.Length); // clear

            if (method != null)
            {
                builder.Append(method.ReflectedType.FullName);
                string methodName = method.Name;

                if (methodName.Length > 0 && methodName[0] != '.')
                {
                    builder.Append('.');
                }

                builder.Append(methodName);
            }
            else
            {
                System.Diagnostics.Debug.Assert(false, "Null current method detected.");
            }

            return builder.ToString();
        }

        /// <summary>
        /// Allow caller to remove a standard prefix from some/all of the display method names.
        /// </summary>
        public static string MethodNameIgnorePrefix { get; set; }

        /// <summary>
        /// Retrieves a trace context and logs an entry trace statement.
        /// </summary>
        /// <param name="method">The current method being traced. Used to provide the function
        /// name prefix.</param>
        /// <returns>The trace context to trace with.</returns>
        public static FTrace Enter(MethodBase method)
        {
            return FTrace.Enter(method, null, null);
        }

        /// <summary>
        /// Retrieves a trace context and logs an entry trace statement. This override also
        /// allows you to log a custom string so you can trace the values of function input
        /// parameters.
        /// </summary>
        /// <param name="method">The current method being traced. Used to provide the function
        /// name prefix.</param>
        /// <param name="format">Optional format string to log the values of input params.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        /// <returns>The trace context to trace with.</returns>
        public static FTrace Enter(MethodBase method, string format, params object[] args)
        {
            FTrace context = GetTraceContext(method);

            context.TraceEntry(format, args);

            return context;
        }

        /// <summary>
        /// Retrieves a trace context and logs an entry trace statement.
        /// </summary>
        /// <param name="method">The current method being traced. Used to provide the function
        /// name prefix.</param>
        /// <param name="level">The trace level to append to the trace categories. The overrides
        /// without this parameter just use categories: Entry/Trace/Exit. By specifying a trace
        /// level each of these categories will have this level number appended to it.</param>
        /// <returns>The trace context to trace with.</returns>
        public static FTrace Enter(MethodBase method, uint level)
        {
            return FTrace.Enter(method, level, null, null);
        }

        /// <summary>
        /// Retrieves a trace context and logs an entry trace statement.
        /// </summary>
        /// <param name="method">The current method being traced. Used to provide the function
        /// name prefix.</param>
        /// <param name="level">The trace level to append to the trace categories. The overrides
        /// without this parameter just use categories: Entry/Trace/Exit. By specifying a trace
        /// level each of these categories will have this level number appended to it.</param>
        /// <param name="format">Optional format string to log the values of input params.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        /// <returns>The trace context to trace with.</returns>
        public static FTrace Enter(MethodBase method, uint level, string format, params object[] args)
        {
            FTrace context = GetTraceContext(method);

            // Override default setup options.
            context.m_category = level.ToString("Trace#", CultureInfo.InvariantCulture).Trim();
            context.m_level = level;

            // Log entry trace.
            context.TraceEntry(format, args);

            return context;
        }

        /// <summary>
        /// Log a trace statement using the default category that was setup by your call to
        /// Enter, i.e. Trace if you did not supply a level, otherwise Trace# where the hash is
        /// the level you supplied.
        /// </summary>
        /// <param name="format">The format string for the trace statement.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        public void Log(string format, params object[] args)
        {
            TraceGeneric(m_category, format, args);
        }

        /// <summary>
        /// Logs a trace statement, but overrides the default category with some custom category.
        /// </summary>
        /// <param name="category">The custom category to use for this trace statement.</param>
        /// <param name="format">The format string for the trace statement.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        /// <remarks>We need a new name otherwise in places where we mean to call
        /// Log(string, params) above we can end up calling this one by mistake.</remarks>
        public void Log2(string category, string format, params object[] args)
        {
            TraceGeneric(category, format, args);
        }

        /// <summary>
        /// Log a trace statement that overrides the level you setup when you called Enter, i.e.
        /// Trace# where hash is the level you have supplied in this call.
        /// </summary>
        /// <param name="level">The override for the level.</param>
        /// <param name="format">The format string for the trace statement.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        public void Log(uint level, string format, params object[] args)
        {
            TraceGeneric(level.ToString("Trace#", CultureInfo.InvariantCulture).Trim(), format, args);
        }

        /// <summary>
        /// Logs a debug trace statement using the "Debug" category. This statement will only
        /// be traced for debug builds.
        /// </summary>
        /// <param name="format">The format string for the trace statement.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        [Conditional("DEBUG")]
        public void Debug(string format, params object[] args)
        {
            TraceGeneric(DebugCategory, format, args);
        }

        /// <summary>
        /// Logs a debug trace statement using the "Debug" category. This statement will only
        /// be traced for debug builds. This is a static version for when you don't want entry
        /// exit tracing.
        /// </summary>
        /// <param name="method">The calling method in which the tracing occurs.</param>
        /// <param name="format">The format string for the trace statement.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        [Conditional("DEBUG")]
        public static void Debug(MethodBase method, string format, params object[] args)
        {
            string preface = GetMethodPreface(method);
            TraceGeneric(preface, DebugCategory, format, args);
        }

        /// <summary>
        /// Traces the details of the exception. We use the ToString implementation of the
        /// exception in order to get the stack trace and the exception message.
        /// </summary>
        /// <param name="ex">The exception to trace.</param>
        public void Except(Exception ex)
        {
            if (ex != null)
            {
                TraceGeneric(m_category, ex.ToString());
            }
            else
            {
                System.Diagnostics.Debug.Assert(false, "Null exception supplied.");
            }
        }

        /// <summary>
        /// Traces the details of the exception. We use the ToString implementation of the
        /// exception in order to get the stack trace and the exception message.
        /// </summary>
        /// <param name="method">The calling method in which the tracing occurs.</param>
        /// <param name="ex">The exception to trace.</param>
        public static void Except(MethodBase method, Exception ex)
        {
            if (ex != null)
            {
                string preface = GetMethodPreface(method);
                TraceGeneric(preface, DefaultCategory, "{0}", ex.ToString());
            }
            else
            {
                System.Diagnostics.Debug.Assert(false, "Null exception supplied.");
            }
        }

        /// <summary>
        /// Allows you to log a single trace statement without requiring a prior call to Entry,
        /// i.e. you don't want to be forced into including Entry/Exit tracing. While you could
        /// call Trace.WriteLine directly this is a little more convenient as it formats the
        /// method name into the trace statement as well as allowing you to supply a format
        /// string which Trace does not.
        /// </summary>
        /// <param name="method">The method in which you are tracing.</param>
        /// <param name="category">The trace category to use.</param>
        /// <param name="format">The optional format string.</param>
        /// <param name="args">The variable arguments that match the format string.</param>
        public static void Log(MethodBase method, string category, string format, params object[] args)
        {
            string preface = GetMethodPreface(method);
            TraceGeneric(preface, category, format, args);
        }

        /// <summary>
        /// Traces a single statement using the "Trace1" category.
        /// </summary>
        /// <param name="method">The method in which you are tracing.</param>
        /// <param name="format">The format string.</param>
        /// <param name="args">The variable arguments that match the format string.</param>
        public static void Trace1(MethodBase method, string format, params object[] args)
        {
            string preface = GetMethodPreface(method);
            TraceGeneric(preface, "Trace1", format, args);
        }

        /// <summary>
        /// Traces a single statement using the "Trace1" category.
        /// </summary>
        /// <param name="format">The format string.</param>
        /// <param name="args">The variable arguments that match the format string.</param>
        public void Trace1(string format, params object[] args)
        {
            Log(1, format, args);
        }

        /// <summary>
        /// Traces a single statement using the "Trace5" category.
        /// </summary>
        /// <param name="method">The method in which you are tracing.</param>
        /// <param name="format">The format string.</param>
        /// <param name="args">The variable arguments that match the format string.</param>
        public static void Trace5(MethodBase method, string format, params object[] args)
        {
            string preface = GetMethodPreface(method);
            TraceGeneric(preface, "Trace5", format, args);
        }

        /// <summary>
        /// Traces a single statement using the "Trace5" category.
        /// </summary>
        /// <param name="format">The format string.</param>
        /// <param name="args">The variable arguments that match the format string.</param>
        public void Trace5(string format, params object[] args)
        {
            Log(5, format, args);
        }

        /// <summary>
        /// Traces a single statement using the "Trace9" category.
        /// </summary>
        /// <param name="method">The method in which you are tracing.</param>
        /// <param name="format">The format string.</param>
        /// <param name="args">The variable arguments that match the format string.</param>
        public static void Trace9(MethodBase method, string format, params object[] args)
        {
            string preface = GetMethodPreface(method);
            TraceGeneric(preface, "Trace9", format, args);
        }

        /// <summary>
        /// Traces a single statement using the "Trace9" category.
        /// </summary>
        /// <param name="format">The format string.</param>
        /// <param name="args">The variable arguments that match the format string.</param>
        public void Trace9(string format, params object[] args)
        {
            Log(9, format, args);
        }

        /// <summary>
        /// Logs an Exit trace statement and pushes this FTrace instance into the cache for reuse.
        /// </summary>
        public void Exit()
        {
            TraceExit(null, null);
        }

        /// <summary>
        /// Logs an Exit trace statement and pushes this FTrace instance into the cache for reuse.
        /// This override also allows you to trace a custom string, which is generally expected
        /// to be the values of output parameters.
        /// </summary>
        /// <param name="format">Optional format string.</param>
        /// <param name="args">Variable argument list that matches the format string.</param>
        public void Exit(string format, params object[] args)
        {
            TraceExit(format, args);
        }

        /// <summary>
        /// Writes a trace statement for function entry.
        /// </summary>
        /// <param name="format">Optional format string for incoming params.</param>
        /// <param name="args">Values of incoming params that match the format string.</param>
        private void TraceEntry(string format, params object[] args)
        {
            if (m_level.HasValue)
            {
                TraceGeneric(m_level.Value.ToString("Entry#", CultureInfo.InvariantCulture).Trim(), format, args);
            }
            else
            {
                TraceGeneric("Entry", format, args);
            }
        }

        /// <summary>
        /// Writes a trace statement for function exit. We also push this FTrace instance onto
        /// the cache for reuse.
        /// </summary>
        /// <param name="format">Optional format string for outgoing param values.</param>
        /// <param name="args">Values of the outgoing params that match the format string.</param>
        private void TraceExit(string format, params object[] args)
        {
            if (m_level.HasValue)
            {
                TraceGeneric(m_level.Value.ToString("Exit#", CultureInfo.InvariantCulture).Trim(), format, args);
            }
            else
            {
                TraceGeneric("Exit", format, args);
            }

            if (m_cachedContext != null)
            {
                m_cachedContext.Enqueue(this);
            }
            else
            {
                System.Diagnostics.Debug.Assert(false, "FTrace cache not initialized");
            }
        }

        /// <summary>
        /// Generic tracing instance function. We just route to the static version which does
        /// the leg work.
        /// </summary>
        /// <param name="category">Required trace category.</param>
        /// <param name="format">Optional format string.</param>
        /// <param name="args">Params that match the format string.</param>
        private void TraceGeneric(string category, string format, params object[] args)
        {
            TraceGeneric(m_function, category, format, args);
        }

        /// <summary>
        /// Generic tracing function that does the bulk of the legwork for our internal tracing.
        /// Every trace statement gets prepended by the function name.
        /// </summary>
        /// <param name="preface">The fully qualified function name (preface) to the trace
        /// statement.</param>
        /// <param name="category">Required trace category.</param>
        /// <param name="format">Optional format string.</param>
        /// <param name="args">Params that match the format string.</param>
        private static void TraceGeneric(string preface, string category, string format, params object[] args)
        {
            // check for standard prefix to be ignored
            if (MethodNameIgnorePrefix != null &&
                preface.StartsWith(MethodNameIgnorePrefix, StringComparison.OrdinalIgnoreCase))
            {
                // replace standard prefix with truncation indicator
                preface = @"+" + preface.Substring(MethodNameIgnorePrefix.Length);
            }

            try
            {
                if (null == format)
                {
                    Trace.WriteLine(preface, category);
                }
                else
                {
                    try
                    {
                        StringBuilder builder = Builder;

                        builder.Remove(0, builder.Length);
                        builder.Append(preface);
                        builder.Append(" ");
                        builder.AppendFormat(CultureInfo.InvariantCulture, format, args);

                        Trace.WriteLine(builder.ToString(), category);
                    }
                    catch (ArgumentOutOfRangeException)
                    {
                        Trace.WriteLine("Trace string too large", TraceErrorCategory);
                    }
                    catch (ArgumentNullException)
                    {
                        Trace.WriteLine("Null arguments supplied for tracing", TraceErrorCategory);
                    }
                    catch (FormatException)
                    {
                        Trace.WriteLine("Unable to format the supplied trace message", TraceErrorCategory);
                    }
                }
            }
            catch
            {
                // Ignore all exceptions to trace. We don't want to propogate exception
                // up the call stack when tracing fails.
            }
        }
    }
}
